package org.processmining.plugins.complianceframework;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.deckfour.xes.classification.XEventClass;
import org.deckfour.xes.extension.std.XConceptExtension;
import org.deckfour.xes.model.XLog;
import org.processmining.contexts.uitopia.UIPluginContext;
import org.processmining.contexts.uitopia.annotations.UITopiaVariant;
import org.processmining.framework.connections.ConnectionCannotBeObtained;
import org.processmining.framework.plugin.annotations.Plugin;
import org.processmining.models.guards.Expression;
import org.processmining.models.connections.petrinets.behavioral.InitialMarkingConnection;
import org.processmining.models.graphbased.directed.petrinet.InhibitorNet;
import org.processmining.models.graphbased.directed.petrinet.PetrinetGraph;
import org.processmining.models.graphbased.directed.petrinet.elements.Transition;
import org.processmining.models.graphbased.directed.petrinetwithdata.newImpl.DataElement;
import org.processmining.models.graphbased.directed.petrinetwithdata.newImpl.PNWDTransition;
import org.processmining.models.graphbased.directed.petrinetwithdata.newImpl.PetriNetWithData;
import org.processmining.models.semantics.petrinet.Marking;
import org.processmining.plugins.DataConformance.Alignment;
import org.processmining.plugins.DataConformance.ResultReplay;
import org.processmining.plugins.DataConformance.DataAlignment.PetriNet.DataAlignment;
import org.processmining.plugins.DataConformance.DataAlignment.PetriNet.ResultReplayPetriNetWithData;
import org.processmining.plugins.DataConformance.framework.ExecutionStep;
import org.processmining.plugins.compliance.AbstractLogForCompliance_Plugin;
import org.processmining.plugins.compliance.align.PNLogReplayer;
import org.processmining.plugins.compliance.align.PNLogReplayer.ReplayParams;
import org.processmining.plugins.compliance.temporal.CreateTemporalPattern_Plugin;
import org.processmining.plugins.connectionfactories.logpetrinet.TransEvClassMapping;
import org.processmining.plugins.petrinet.replayresult.PNRepResult;

public class DeviationOverviewPlugin {
	private static int maxIntervalSize = 120000;

	@Plugin(name = "Get Compliance dashboard", parameterLabels = { "Log",
			"Data Petri Net" }, returnLabels = { "Compliance Dashboard" }, returnTypes = { ComplianceDashboard.class })
	@UITopiaVariant(affiliation = UITopiaVariant.EHV, author = "V. Gromov", email = "v.gromov@student.tue.nl")
	public ComplianceDashboard getDeviationOverview(UIPluginContext context,
			XLog log, PetriNetWithData DPNet) {
		// TODO: ask user before running task, detect/ask resource variable, ask
		// about intervals

		Object[] replayOutput = replayLog(context, (InhibitorNet) DPNet, log);
		// get results of the replay
		PNRepResult res = (PNRepResult) replayOutput[0];
		TransEvClassMapping mapping = (TransEvClassMapping) replayOutput[1];
		XLog abstractedLog = (XLog) replayOutput[2];

		// register them in the framework
		context.getProvidedObjectManager().createProvidedObject(
				"Control-flow compliance alignment for "
						+ XConceptExtension.instance().extractName(
								abstractedLog) + " on " + DPNet.getLabel(),
				res, PNRepResult.class, context);
		context.getProvidedObjectManager().createProvidedObject(
				"Mapping between "
						+ XConceptExtension.instance().extractName(
								abstractedLog) + " and " + DPNet.getLabel(),
				mapping, TransEvClassMapping.class, context);

		DataAlignment dataAligner = new DataAlignment();
		ResultReplayPetriNetWithData repResWithData = dataAligner.plugin(
				context, DPNet, abstractedLog);

		context.getProvidedObjectManager().createProvidedObject(
				repResWithData.toString(), repResWithData,
				ResultReplayPetriNetWithData.class, context);

		Map<String, float[]> intervals = getIntervals(DPNet, repResWithData);

		org.processmining.plugins.compliance.temporal.CheckComplianceReplayer_Plugin temporalCompliancePlugin = new org.processmining.plugins.compliance.temporal.CheckComplianceReplayer_Plugin();
		// ResultReplay repResTemporal =
		// (ResultReplay)temporalCompliancePlugin.replayLog(context, res)[0];
		// TODO: register time-aware in the framework?

		// ComplianceDashboard result = new ComplianceDashboard(res,
		// repResWithData, repResTemporal, false, null, interVals, context);
		ComplianceDashboard result = new ComplianceDashboard(res,
				repResWithData, null, false, null, intervals, context);
		return result;
	}

	// get control-flow alignment in almost the same way, as in compliance
	// checking
	private Object[] replayLog(UIPluginContext context, PetrinetGraph net,
			XLog originalLog) {
		for (Transition t : net.getTransitions()) {
			if (t.getLabel().toLowerCase().equalsIgnoreCase("tau")
					|| t.getLabel().toLowerCase().startsWith("final")
					|| t.getLabel()
							.toLowerCase()
							.equals(CreateTemporalPattern_Plugin.T_INSTANCE_START
									.toLowerCase())
					|| t.getLabel()
							.toLowerCase()
							.equals(CreateTemporalPattern_Plugin.T_INSTANCE_COMPLETE
									.toLowerCase())
					|| t.getLabel()
							.toLowerCase()
							.equals(CreateTemporalPattern_Plugin.T_START
									.toLowerCase())
					|| t.getLabel()
							.toLowerCase()
							.equals(CreateTemporalPattern_Plugin.T_END
									.toLowerCase())) {
				t.setInvisible(true);
			}
		}

		Object[] abstraction = AbstractLogForCompliance_Plugin
				.connectLogToPetriNet(context, originalLog, net);
		XLog log = (XLog) abstraction[0];
		TransEvClassMapping mapping = (TransEvClassMapping) abstraction[1];

		Marking m_initial;
		try {
			m_initial = context.tryToFindOrConstructFirstObject(Marking.class,
					InitialMarkingConnection.class,
					InitialMarkingConnection.MARKING, net);
		} catch (ConnectionCannotBeObtained e) {
			return null;
		}

		Marking m_final = (Marking) PNLogReplayer.constructFinalMarking(
				context, net)[1];

		ReplayParams par = PNLogReplayer.getReplayerParameters(context, net,
				log, mapping.getEventClassifier(), m_initial, m_final);
		for (XEventClass e : par.mapEvClass2Cost.keySet()) {
			par.mapEvClass2Cost.put(e, 100);
		}
		par.parameters.setCreateConn(true);
		PNRepResult res = PNLogReplayer.callReplayer(context, net, log,
				mapping, par);

		context.getFutureResult(0).setLabel(
				"Control-flow compliance alignment for "
						+ XConceptExtension.instance().extractName(log)
						+ " on " + net.getLabel() + " using "
						+ par.selectedAlg.toString());

		return new Object[] { res, mapping, log };
	}

	private Map<String, float[]> getIntervals(PetriNetWithData DPNet,
			ResultReplayPetriNetWithData dataAwareReplay) {
		Map<String, float[]> result = new HashMap<String, float[]>();
		for (DataElement variable : DPNet.getVariables()) {
			if (!variable.getType().equals(Float.class)
					&& !variable.getType().equals(Integer.class))
				continue;
			List<Float> valuesList = new ArrayList<Float>();
			boolean intervalsInDPN = true;
			for (Transition t : DPNet.getTransitions()) {
				Expression expression = ((PNWDTransition) t).getGuard();
				if (expression == null)
					continue;
				if (!extractGuardNumbers(expression, variable, valuesList)) {
					intervalsInDPN = false;
					break;
				}
			}
			if (!intervalsInDPN)
				result.put(variable.getVarName(),
						getIntervalsFromLog(variable, dataAwareReplay));
			else {
				valuesList.add(Float.MAX_VALUE);
				for (int i = 0; i < valuesList.size() - 1; i++) {
					for (int j = i + 1; j < valuesList.size(); j++) {
						if (valuesList.get(i).floatValue() == valuesList.get(j)
								.floatValue()) {
							valuesList.remove(j);
							j -= 1;
						}
					}
				}
				float[] values = new float[valuesList.size()];
				for (int i = 0; i < values.length; i++) {
					values[i] = valuesList.get(i).floatValue();
				}
				Arrays.sort(values);
				result.put(variable.getVarName(), values);
			}
		}
		return result;
	}

	private float[] getIntervalsFromLog(DataElement variable,
			ResultReplayPetriNetWithData dataAwareReplay) {
		float min = -Float.MAX_VALUE, max = Float.MAX_VALUE;
		for (Alignment alignment : dataAwareReplay.labelStepArray) {
			for (ExecutionStep LogStep : alignment.getLogTrace()) {
				Object varVal = LogStep.get(variable.getVarName());
				if (varVal == null)
					continue;
				float value = Float.parseFloat(varVal.toString());
				if (value > max) {
					max = value;
				}
				if (value < min) {
					min = value;
				}
			}
		}
		if (max == Float.MAX_VALUE && min == (-Float.MAX_VALUE))
			return null;
		if (min == max) {
			return new float[] { min, Float.MAX_VALUE };
		}

		int numIntervals = 10;
		float intervalSize = (max - min) / numIntervals;
		if ((int) intervalSize > maxIntervalSize) {
			intervalSize = maxIntervalSize;
			numIntervals = (int) ((max - min) / intervalSize);
			if (numIntervals * intervalSize < max) {
				numIntervals += 1;
			}
		}
		float[] result = new float[numIntervals + 2];
		for (int i = 0; i < result.length - 1; i++) {
			result[i] = min + intervalSize * (float) i;
		}
		result[result.length] = Float.MAX_VALUE;

		return result;
	}

	private boolean extractGuardNumbers(Expression expression,
			DataElement variable, List<Float> extractedNumbers) {

		if (expression.isComparisonExpression()) {
			if (getVariablesCount(expression) > 1)
				return false;
			if (expression.getOperand1() instanceof Expression) {
				if (expression.getOperand2() instanceof Expression) {
					return extractGuardNumbers(
							(Expression) expression.getOperand1(), variable,
							extractedNumbers)
							& extractGuardNumbers(
									(Expression) expression.getOperand2(),
									variable, extractedNumbers);
				} else if (expression.getOperand2() instanceof Number) {
					boolean operand1 = extractGuardNumbers(
							(Expression) expression.getOperand1(), variable,
							extractedNumbers);
					if (operand1)
						extractedNumbers
								.add(((Number) expression.getOperand2())
										.floatValue());
					return operand1;
				} else if (expression.getOperand2() instanceof String
						&& ((String) expression.getOperand2()).equals(variable
								.getLabel())) {
					return extractGuardNumbers(
							(Expression) expression.getOperand1(), variable,
							extractedNumbers);
				} else
					return false;
			} else if (expression.getOperand1() instanceof String
					&& ((String) expression.getOperand1()).equals(variable
							.getVarName())) {
				if (expression.getOperand1() instanceof Expression) {
					return extractGuardNumbers(
							(Expression) expression.getOperand2(), variable,
							extractedNumbers);
				} else if (expression.getOperand2() instanceof Number) {
					extractedNumbers.add(((Number) expression.getOperand2())
							.floatValue());
					return true;
				} else
					return false;
			} else if (expression.getOperand1() instanceof Number) {
				if (expression.getOperand2() instanceof Expression) {
					boolean operand2 = extractGuardNumbers(
							(Expression) expression.getOperand2(), variable,
							extractedNumbers);
					if (operand2)
						extractedNumbers
								.add(((Number) expression.getOperand1())
										.floatValue());
					return operand2;
				} else if (expression.getOperand2() instanceof String
						&& ((String) expression.getOperand2()).equals(variable
								.getVarName())) {
					extractedNumbers.add(((Number) expression.getOperand1())
							.floatValue());
					return true;
				} else
					return false;
			} else
				return false;
		} else if (expression.isBooleanExpression()) {
			boolean operand1 = true, operand2 = true;
			if (expression.getOperand1() instanceof Expression
					&& containsVariable((Expression) expression.getOperand1(),
							variable)) {
				operand1 = extractGuardNumbers(
						(Expression) expression.getOperand1(), variable,
						extractedNumbers);
			}
			if (expression.getOperand2() instanceof Expression
					&& containsVariable((Expression) expression.getOperand2(),
							variable)) {
				operand2 = extractGuardNumbers(
						(Expression) expression.getOperand2(), variable,
						extractedNumbers);
			}
			return operand1 & operand2;
		} else if (expression.isMathematicalExpression()) {
			if (expression.findVariables().hasMoreElements())
				return true;
			Float expResult = calcMathExpression(expression);
			if (expResult != null) {
				extractedNumbers.add(expResult.floatValue());
				return true;
			} else
				return false;
		} else if (expression.getOperand1() instanceof Expression
				&& expression.getOperator() == null) {
			return extractGuardNumbers((Expression) expression.getOperand1(),
					variable, extractedNumbers);
		} else
			return true;

	}

	private Float calcMathExpression(Expression expression) {
		Float result = 0.0f;
		try {
			if ((expression.getOperand1() == null && expression.getOperand2() == null)
					|| (expression.getOperator() != null && (expression
							.getOperand1() == null || expression.getOperand2() == null)))
				return null;
			if (expression.getOperator() == null) {
				if ((expression.getOperand1() != null && expression
						.getOperand2() != null))
					return null;
				else if (expression.getOperand1() != null
						&& expression.getOperand1() instanceof Number) {
					return (((Number) expression.getOperand1()).floatValue());
				} else if (expression.getOperand2() != null
						&& expression.getOperand2() instanceof Number) {
					return (((Number) expression.getOperand2()).floatValue());
				} else
					return null;
			}

			Float operand1, operand2;
			if (expression.getOperand1() instanceof Expression) {
				operand1 = calcMathExpression(expression);
			} else if (expression.getOperand1() instanceof Number)
				operand1 = ((Number) expression.getOperand1()).floatValue();
			else
				return null;

			if (expression.getOperand2() instanceof Expression) {
				operand2 = calcMathExpression(expression);
			} else if (expression.getOperand2() instanceof Number)
				operand2 = ((Number) expression.getOperand2()).floatValue();
			else
				return null;

			if (operand1 == null || operand2 == null)
				return null;

			if (expression.getOperator().equals("+"))
				result = operand1.floatValue() + operand2.floatValue();
			else if (expression.getOperator().equals("-"))
				result = operand1.floatValue() - operand2.floatValue();
			else if (expression.getOperator().equals("*"))
				result = operand1.floatValue() * operand2.floatValue();
			else if (expression.getOperator().equals("/"))
				result = operand1.floatValue() / operand2.floatValue();
			else if (expression.getOperator().equals("%"))
				result = operand1.floatValue() % operand2.floatValue();

			return result;
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
	}

	private int getVariablesCount(Expression expression) {
		int i = 0;
		Enumeration<String> vars = expression.findVariables();
		while (vars.hasMoreElements()) {
			i++;
			vars.nextElement();
		}
		return i;
	}

	private boolean containsVariable(Expression expression, DataElement variable) {
		boolean containsVariable = false;
		Enumeration<String> vars = expression.findVariables();
		while (vars.hasMoreElements()) {
			if (vars.nextElement().toString()
					.equalsIgnoreCase(variable.getLabel())) {
				containsVariable = true;
				break;
			}
		}
		if (!containsVariable)
			return true;
		return false;
	}
}